{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Subspace-search Variational Quantum Eigensolver\n",
    "\n",
    "<em> Copyright (c) 2021 Institute for Quantum Computing, Baidu Inc. All Rights Reserved. </em>\n",
    "\n",
    "## Overview\n",
    "\n",
    "- In this tutorial, we will show how to train a quantum neural network (QNN) through Paddle Quantum to find the entire energy spectrum of a quantum system.\n",
    "\n",
    "- First, import the following packages."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-04-30T09:12:27.747028Z",
     "start_time": "2021-04-30T09:12:25.171248Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy\n",
    "from numpy import pi as PI\n",
    "import paddle \n",
    "import paddle_quantum\n",
    "from paddle import matmul\n",
    "from paddle_quantum.ansatz import Circuit\n",
    "from paddle_quantum.qinfo import random_pauli_str_generator, pauli_str_to_matrix\n",
    "from paddle_quantum.linalg import dagger"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Background\n",
    "\n",
    "- Variational Quantum Eigensolver (VQE) [1-3] is one of the most promising applications for near-term quantum computing. One of the its powerful versions is SSVQE [4], which can be used to find the ground state and the **excited state** of a physical system's Hamiltonian. Mathematically, one can interpret it as solving the eigenvalues and eigenvectors of a Hermitian matrix. The set of eigenvalues of the Hamiltonian is called the energy spectrum.\n",
    "- Next, we will use a brief example to demonstrate how to solve this problem by training a QNN, that is, to solve the energy spectrum of a given Hamiltonian $H$.\n",
    "\n",
    "## Hamiltonian \n",
    "\n",
    "- For a specific molecule that needs to be analyzed, we need its geometry, charge, and spin multiplicity to obtain the Hamiltonian (in Pauli products form) describing the system. Specifically, through our built-in quantum chemistry toolkit, fermionic-to-qubit mapping technology can be used to output the qubit Hamiltonian.\n",
    "- As a simple demonstration of SSVQE, we provide a random 2-qubit Hamiltonian."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-04-30T09:12:27.773417Z",
     "start_time": "2021-04-30T09:12:27.752568Z"
    }
   },
   "outputs": [],
   "source": [
    "N = 2  # Number of qubits\n",
    "SEED = 14  # Fixed random seed"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-04-30T09:12:27.804177Z",
     "start_time": "2021-04-30T09:12:27.779339Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Random Hamiltonian in Pauli string format = \n",
      " [[0.9152074787317819, 'x1,y0'], [-0.2717604556798945, 'z0'], [0.3628495008719168, 'x0'], [-0.5050129214094752, 'x1'], [-0.6971554357833791, 'y0,x1'], [0.8651151857574237, 'x0,y1'], [0.7409989105435002, 'y0'], [-0.39981603921243236, 'y0'], [0.06862640764702, 'z0'], [-0.7647553733438246, 'y1']]\n"
     ]
    }
   ],
   "source": [
    "# Generate random Hamiltonian represented by Pauli string\n",
    "numpy.random.seed(SEED)\n",
    "hamiltonian = random_pauli_str_generator(N, terms=10)\n",
    "print(\"Random Hamiltonian in Pauli string format = \\n\", hamiltonian)\n",
    "\n",
    "# Generate matrix representation of Hamiltonian\n",
    "complex_dtype = paddle_quantum.get_dtype()\n",
    "H = pauli_str_to_matrix(hamiltonian, N).astype(complex_dtype)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Building a quantum neural network\n",
    "\n",
    "- To implement SSVQE, we first need to design a QNN $U(\\theta)$ (parameterized quantum circuit). In this tutorial, we provide a predefined universal quantum circuit template suitable for 2 qubits. Theoretically, this template has enough expressibility to simulate arbitrary 2-qubit unitary operation [5]. The specific implementation requires 3 $CNOT$ gates plus 15 single-qubit rotation gates $\\in \\{R_y, R_z\\}$.\n",
    "\n",
    "- One can randomly initialize the QNN parameters ${\\bf{\\vec{\\theta }}}$ containing 15 parameters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-04-30T09:12:27.822250Z",
     "start_time": "2021-04-30T09:12:27.809696Z"
    }
   },
   "outputs": [],
   "source": [
    "def U_theta(num_qubits: int) -> Circuit:\n",
    "    \"\"\"\n",
    "    U_theta\n",
    "    \"\"\"\n",
    "    # Initialize the quantum neural network according to the number of qubits/network width\n",
    "    cir = Circuit(num_qubits)\n",
    "    \n",
    "    # Call the built-in quantum neural network template\n",
    "    cir.universal_two_qubits([0, 1])\n",
    "\n",
    "    # Return the circuit of the quantum neural network\n",
    "    return cir"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Training model and loss function\n",
    "\n",
    "- After setting up the Hamiltonian and the quantum neural network architecture, we will further define the parameters to be trained, the loss function and optimization methods. For a detailed inspection of the theory of SSVQE, please refer to the original paper [4].\n",
    "\n",
    "- By acting the quantum neural network $U(\\theta)$ on a set of orthogonal initial states (one can take the computational basis $\\{|00\\rangle, |01\\rangle, |10\\rangle, |11 \\rangle \\}$), we will get the output states $\\{\\left| {\\psi_1 \\left( {\\bf{\\theta }} \\right)} \\right\\rangle, \\left| {\\psi_2 \\left( {\\bf{\\theta }} \\right)} \\right\\rangle, \\left| {\\psi_3 \\left( {\\bf{\\theta }} \\right)} \\right\\rangle, \\left| {\\psi_4 \\left( {\\bf{\\theta }} \\right)} \\right\\rangle \\}$.\n",
    "\n",
    "- Further, the loss function in the SSVQE model generally consists of expectation value of each output quantum state $\\left| {\\psi_k \\left( {\\bf{\\theta }} \\right)} \\right\\rangle$ given the Hamiltonian $H$. More specifically, it's the weighted summation of the energy expectation value. In this example, the default weight vector is $\\vec{w} = [4, 3, 2, 1]$.\n",
    "\n",
    "- The loss function is defined as:\n",
    "\n",
    "$$\n",
    "\\mathcal{L}(\\boldsymbol{\\theta}) = \\sum_{k=1}^{2^n}w_k*\\left\\langle {\\psi_k \\left( {\\bf{\\theta }} \\right)} \\right|H\\left| {\\psi_k \\left( {\\bf{\\theta }} \\right)} \\right\\rangle. \\tag{1}\n",
    "$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-04-30T09:12:28.432737Z",
     "start_time": "2021-04-30T09:12:28.423798Z"
    }
   },
   "outputs": [],
   "source": [
    "class Net(paddle.nn.Layer):\n",
    "    def __init__(self, num_qubits: int):\n",
    "        super(Net, self).__init__()\n",
    "        \n",
    "        # Build quantum neural network\n",
    "        self.cir = U_theta(num_qubits)\n",
    "\n",
    "    # Define loss function and forward propagation mechanism\n",
    "    def forward(self, H, N):\n",
    "        \n",
    "        # Calculate the loss function\n",
    "        U = self.cir.unitary_matrix()\n",
    "        loss_struct = paddle.real(matmul(matmul(dagger(U), H), U))\n",
    "\n",
    "        # Enter the computational basis to calculate the expected value \n",
    "        # which is equivalent to taking the diagonal element of U^dagger*H*U\n",
    "        loss_components = []\n",
    "        for i in range(len(loss_struct)):\n",
    "            loss_components.append(loss_struct[i][i])\n",
    "        \n",
    "        # Weighted summation of loss function\n",
    "        loss = 0\n",
    "        for i in range(len(loss_components)):\n",
    "            weight = 4 - i\n",
    "            loss += weight * loss_components[i]\n",
    "        \n",
    "        return loss, loss_components, self.cir"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Hyper-parameters\n",
    "\n",
    "Before training the quantum neural network, we also need to set up several hyper-parameters, mainly the learning rate LR, the number of iterations ITR. Here we set the learning rate to be LR = 0.3 and the number of iterations ITR = 100. One can adjust these hyper-parameters accordingly and check how they influence the training performance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-04-30T09:12:29.579180Z",
     "start_time": "2021-04-30T09:12:29.575632Z"
    }
   },
   "outputs": [],
   "source": [
    "ITR = 100  # Set the total number of iterations of training\n",
    "LR = 0.3  # Set the learning rate"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Training process\n",
    "\n",
    "- After setting all the parameters of SSVQE model, we need to convert all the data into Tensor in the PaddlePaddle, and then train the quantum neural network.\n",
    "- We use Adam Optimizer in training, and one can also call other optimizers provided in PaddlePaddle."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-04-30T09:12:44.010556Z",
     "start_time": "2021-04-30T09:12:41.952650Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "iter: 10 loss: -4.5668\n",
      "iter: 20 loss: -5.3998\n",
      "iter: 30 loss: -5.6210\n",
      "iter: 40 loss: -5.8872\n",
      "iter: 50 loss: -5.9246\n",
      "iter: 60 loss: -5.9471\n",
      "iter: 70 loss: -5.9739\n",
      "iter: 80 loss: -5.9833\n",
      "iter: 90 loss: -5.9846\n",
      "iter: 100 loss: -5.9848\n"
     ]
    }
   ],
   "source": [
    "paddle.seed(SEED)\n",
    "\n",
    "# We need to convert numpy.ndarray to Tensor supported in Paddle\n",
    "hamiltonian = paddle.to_tensor(H)\n",
    "\n",
    "# Determine the parameter dimension of the network\n",
    "net = Net(N)\n",
    "\n",
    "# We use Adam optimizer for better performance\n",
    "# One can change it to SGD or RMSprop.\n",
    "opt = paddle.optimizer.Adam(learning_rate=LR, parameters=net.parameters())\n",
    "\n",
    "# Optimization loop\n",
    "for itr in range(1, ITR + 1):\n",
    "\n",
    "    # Forward propagation calculates the loss function and returns the estimated energy spectrum\n",
    "    loss, loss_components, cir = net(hamiltonian, N)\n",
    "\n",
    "    # Under the dynamic graph mechanism, use back propagation to minimize the loss function\n",
    "    loss.backward()\n",
    "    opt.minimize(loss)\n",
    "    opt.clear_grad()\n",
    "\n",
    "    # Print training results\n",
    "    if itr% 10 == 0:\n",
    "        print('iter:', itr,'loss:','%.4f'% loss.numpy()[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Benchmarking\n",
    "\n",
    "We have now completed the training of the quantum neural network, and we will verify the results by comparing them with theoretical values.\n",
    "- The theoretical Hamiltonian eigenvalues are solved by the linear algebra package in NumPy;\n",
    "- We compare the energy of each energy level obtained by training QNN with the theoretical value.\n",
    "- It can be seen that the training output is very close to the exact value."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-04-30T09:12:45.991342Z",
     "start_time": "2021-04-30T09:12:45.976287Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The estimated ground state energy is:  [-2.1876235]\n",
      "The theoretical ground state energy is:  -2.187902\n",
      "The estimated 1st excited state energy is: [-0.13721023]\n",
      "The theoretical 1st excited state energy is: -0.13704127073287964\n",
      "The estimated 2nd excited state energy is: [0.85251486]\n",
      "The theoretical 2nd excited state energy is: 0.8523274064064026\n",
      "The estimated 3rd excited state energy is: [1.4723194]\n",
      "The theoretical 3rd excited state energy is: 1.4726158380508423\n"
     ]
    }
   ],
   "source": [
    "def output_ordinalvalue(num):\n",
    "    r\"\"\"\n",
    "    Convert to ordinal value\n",
    "\n",
    "    Args:\n",
    "        num (int): input number\n",
    "\n",
    "    Return:\n",
    "        (str): output ordinal value\n",
    "    \"\"\"\n",
    "    if num == 1:\n",
    "        return str(num) + \"st\"\n",
    "    elif num == 2:\n",
    "        return str(num) + \"nd\"\n",
    "    elif num == 3:\n",
    "        return str(num) + \"rd\"\n",
    "    else:\n",
    "        return str(num) + 'th'\n",
    "\n",
    "for i in range(len(loss_components)):\n",
    "    if i == 0:\n",
    "        print('The estimated ground state energy is: ', loss_components[i].numpy())\n",
    "        print('The theoretical ground state energy is: ', numpy.linalg.eigh(H)[0][i])\n",
    "    else:\n",
    "        print('The estimated {} excited state energy is: {}'.format(\n",
    "            output_ordinalvalue(i), loss_components[i].numpy())\n",
    "        )\n",
    "        print('The theoretical {} excited state energy is: {}'.format(\n",
    "            output_ordinalvalue(i), numpy.linalg.eigh(H)[0][i])\n",
    "        )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "_______\n",
    "\n",
    "## References\n",
    "\n",
    "[1] Peruzzo, A. et al. A variational eigenvalue solver on a photonic quantum processor. [Nat. Commun. 5, 4213 (2014).](https://www.nature.com/articles/ncomms5213)\n",
    "\n",
    "[2] McArdle, S., Endo, S., Aspuru-Guzik, A., Benjamin, S. C. & Yuan, X. Quantum computational chemistry. [Rev. Mod. Phys. 92, 015003 (2020).](https://journals.aps.org/rmp/abstract/10.1103/RevModPhys.92.015003)\n",
    "\n",
    "[3] Cao, Y. et al. Quantum chemistry in the age of quantum computing. [Chem. Rev. 119, 10856–10915 (2019).](https://pubs.acs.org/doi/abs/10.1021/acs.chemrev.8b00803)\n",
    "\n",
    "[4] Nakanishi, K. M., Mitarai, K. & Fujii, K. Subspace-search variational quantum eigensolver for excited states. [Phys. Rev. Res. 1, 033062 (2019).](https://journals.aps.org/prresearch/pdf/10.1103/PhysRevResearch.1.033062)\n",
    "\n",
    "[5] Vatan, F. & Williams, C. Optimal quantum circuits for general two-qubit gates. [Phys. Rev. A 69, 032315 (2004).](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.69.032315)"
   ]
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "1ba3360425d54dc61cc146cb8ddc529b6d51be6719655a3ca16cefddffc9595a"
  },
  "kernelspec": {
   "display_name": "Python 3.8.10 64-bit ('paddle_quantum_test': conda)",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.13"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
